#!/usr/bin/env bash
#
# @license Apache-2.0
#
# Copyright (c) 2017 The Stdlib Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# A Git hook called by `git commit`. If this scripts exits with a non-zero status, the commit will be aborted.
#
# This hook is called with no arguments.

# shellcheck disable=SC2181


# VARIABLES #

# Resolve environment variables:
skip_filenames="${SKIP_LINT_FILENAMES}"
skip_markdown="${SKIP_LINT_MARKDOWN}"
skip_package_json="${SKIP_LINT_PACKAGE_JSON}"
skip_repl_help="${SKIP_LINT_REPL_HELP}"
skip_javascript_src="${SKIP_LINT_JAVASCRIPT_SRC}"
skip_javascript_cli="${SKIP_LINT_JAVASCRIPT_CLI}"
skip_javascript_examples="${SKIP_LINT_JAVASCRIPT_EXAMPLES}"
skip_javascript_tests="${SKIP_LINT_JAVASCRIPT_TESTS}"
skip_javascript_benchmarks="${SKIP_LINT_JAVASCRIPT_BENCHMARKS}"
skip_python="${SKIP_LINT_PYTHON}"
skip_r="${SKIP_LINT_R}"
skip_c_src="${SKIP_LINT_C_SRC}"
skip_c_examples="${SKIP_LINT_C_EXAMPLES}"
skip_c_benchmarks="${SKIP_LINT_C_BENCHMARKS}"
skip_c_tests_fixtures="${SKIP_LINT_C_TESTS_FIXTURES}"
skip_shell="${SKIP_LINT_SHELL}"
skip_typescript_declarations="${SKIP_LINT_TYPESCRIPT_DECLARATIONS}"
skip_typescript_tests="${SKIP_LINT_TYPESCRIPT_TESTS}"
skip_license_headers="${SKIP_LINT_LICENSE_HEADERS}"

# Determine root directory:
root=$(git rev-parse --show-toplevel)

# Define the path to a utility for linting filenames:
lint_filenames="${root}/lib/node_modules/@stdlib/_tools/lint/filenames/bin/cli"

# Define the path to a utility for linting package.json files:
lint_package_json="${root}/lib/node_modules/@stdlib/_tools/lint/pkg-json/bin/cli"

# Define the path to a utility for linting REPL help files:
lint_repl_help="${root}/lib/node_modules/@stdlib/_tools/lint/repl-txt/bin/cli"

# Define the path to ESLint configuration file for linting examples:
eslint_examples_conf="${root}/etc/eslint/.eslintrc.examples.js"

# Define the path to ESLint configuration file for linting tests:
eslint_tests_conf="${root}/etc/eslint/.eslintrc.tests.js"

# Define the path to ESLint configuration file for linting benchmarks:
eslint_benchmarks_conf="${root}/etc/eslint/.eslintrc.benchmarks.js"

# Define the path to ESLint configuration file for linting TypeScript definition tests:
eslint_typescript_tests_conf="${root}/etc/eslint/.eslintrc.typescript.tests.js"

# Define the path to cppcheck configuration file for linting examples:
cppcheck_examples_suppressions_list="${root}/etc/cppcheck/suppressions.examples.txt"

# Define the path to cppcheck configuration file for linting test fixtures:
cppcheck_tests_fixtures_suppressions_list="${root}/etc/cppcheck/suppressions.tests_fixtures.txt"

# Define the path to cppcheck configuration file for linting benchmarks:
cppcheck_benchmarks_suppressions_list="${root}/etc/cppcheck/suppressions.benchmarks.txt"


# FUNCTIONS #

# Defines an error handler.
#
# $1 - error status
on_error() {
	cleanup
	exit "$1"
}

# Runs clean-up tasks.
cleanup() {
	echo '' >&2
}

# Runs initialization tasks.
init() {
	return 0
}

# Checks for non-ASCII filenames (to ensure cross platform portability).
check_filenames() {
	local num_files
	local against
	local commit

	commit=$(git rev-parse --verify HEAD)
	if [[ -z "${commit}" ]]; then
		# This is the initial commit, so we diff against an empty tree object:
		against='4b825dc642cb6eb9a060e54bf8d69288fbee4904'
	else
		against='HEAD'
	fi
	# We exploit the fact that the printable range starts with the space character and ends with the tilde. Note that the use of brackets around a `tr` range is okay here, (for portability to Solaris 10's /usr/bin/tr, it's even required), since the square bracket bytes happen to fall in the designated range.
	num_files=$(git diff --cached --name-only --diff-filter=ACR -z "${against}" | LC_ALL=C tr -d '[ -~]\0' | wc -c)

	if [[ "${num_files}" -ne 0 ]]; then
		echo 'Error: Attempting to add a non-ASCII filename. Non-ASCII filenames limit cross-platform portability. Please rename offending files before committing.' >&2
		return 1
	fi
	return 0
}

# Lints staged files.
run_lint() {
	local changed_files
	local files

	# Get the set of changed files (added, copied, modified, and renamed):
	changed_files=$(git diff --name-only --cached --diff-filter ACMR)

	# Lint filenames:
	if [[ -z "${skip_filenames}" ]]; then
		echo "${changed_files}" | "${lint_filenames}"
		if [[ "$?" -ne 0 ]]; then
			echo '' >&2
			echo 'Filename lint errors.' >&2
			return 1
		fi
	fi
	# Lint Markdown files...
	if [[ -z "${skip_markdown}" ]]; then
		files=$(echo "${changed_files}" | grep '\.md$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make FILES="${files}" lint-markdown-files > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'Markdown lint errors.' >&2
				return 1
			fi
		fi
	fi
	# Lint package.json files...
	if [[ -z "${skip_package_json}" ]]; then
		files=$(echo "${changed_files}" | grep 'package\.json$' | grep -v 'datapackage\.json$' )
		if [[ -n "${files}" ]]; then
			echo "${files}" | "${lint_package_json}" >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'Package.json lint errors.' >&2
				return 1
			fi
		fi
	fi
	# Lint REPL help files...
	if [[ -z "${skip_repl_help}" ]]; then
		files=$(echo "${changed_files}" | grep 'repl\.txt$' )
		if [[ -n "${files}" ]]; then
			echo "${files}" | "${lint_repl_help}" >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'REPL help lint errors.' >&2
				return 1
			fi
		fi
	fi
	# Lint JavaScript source files...
	if [[ -z "${skip_javascript_src}" ]]; then
		files=$(echo "${changed_files}" | grep '\.js$' | grep -v -e '/examples' -e '/test' -e '/benchmark' -e '^dist/' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make FILES="${files}" FIX=1 lint-javascript-files > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'JavaScript lint errors for source files.' >&2
				return 1
			fi
		fi
	fi
	# Lint JavaScript command-line interfaces...
	if [[ -z "${skip_javascript_cli}" ]]; then
		files=$(echo "${changed_files}" | grep '/bin/cli$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make FILES="${files}" FIX=1 lint-javascript-files > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'JavaScript lint errors for command-line interface files.' >&2
				return 1
			fi
		fi
	fi
	# Lint JavaScript examples files...
	if [[ -z "${skip_javascript_examples}" ]]; then
		files=$(echo "${changed_files}" | grep '/examples/.*\.js$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make JAVASCRIPT_LINTER=eslint ESLINT_CONF="${eslint_examples_conf}" FILES="${files}" FIX=1 lint-javascript-files > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'JavaScript lint errors for example files.' >&2
				return 1
			fi
		fi
	fi
	# Lint JavaScript test files...
	if [[ -z "${skip_javascript_tests}" ]]; then
		files=$(echo "${changed_files}" | grep '/test/.*\.js$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make JAVASCRIPT_LINTER=eslint ESLINT_CONF="${eslint_tests_conf}" FILES="${files}" FIX=1 lint-javascript-files > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'JavaScript lint errors for test files.' >&2
				return 1
			fi
		fi
	fi
	# Lint JavaScript benchmark files...
	if [[ -z "${skip_javascript_benchmarks}" ]]; then
		files=$(echo "${changed_files}" | grep '/benchmark/.*\.js$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make JAVASCRIPT_LINTER=eslint ESLINT_CONF="${eslint_benchmarks_conf}" FILES="${files}" FIX=1 lint-javascript-files > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'JavaScript lint errors for benchmark files.' >&2
				return 1
			fi
		fi
	fi
	# Lint Python files...
	if [[ -z "${skip_python}" ]]; then
		files=$(echo "${changed_files}" | grep '\.py$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make check-python-linters > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'Unable to lint Python files. Ensure that linters are installed.' >&2
			else
				make FILES="${files}" lint-python-files > /dev/null >&2
				if [[ "$?" -ne 0 ]]; then
					echo '' >&2
					echo 'Python lint errors.' >&2
					return 1
				fi
			fi
		fi
	fi
	# Lint R files...
	if [[ -z "${skip_r}" ]]; then
		files=$(echo "${changed_files}" | grep '\.R$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make FILES="${files}" lint-r-files > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'R lint errors.' >&2
				return 1
			fi
		fi
	fi
	# Lint C source files...
	if [[ -z "${skip_c_src}" ]]; then
		files=$(echo "${changed_files}" | grep '\.c$' | grep -v -e '/examples' -e '/test' -e '/benchmark' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make check-c-linters > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'Unable to lint C files. Ensure that linters are installed.' >&2
			else
				make FILES="${files}" lint-c-files > /dev/null >&2
				if [[ "$?" -ne 0 ]]; then
					echo '' >&2
					echo 'C lint errors for source files.' >&2
					return 1
				fi
			fi
		fi
	fi
	# Lint C examples files...
	if [[ -z "${skip_c_examples}" ]]; then
		files=$(echo "${changed_files}" | grep '/examples/.*\.c$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make check-c-linters > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'Unable to lint C files. Ensure that linters are installed.' >&2
			else
				make C_LINTER=cppcheck CPPCHECK_SUPPRESSIONS_LIST="${cppcheck_examples_suppressions_list}" FILES="${files}" lint-c-files > /dev/null >&2
				if [[ "$?" -ne 0 ]]; then
					echo '' >&2
					echo 'C lint errors for examples files.' >&2
					return 1
				fi
			fi
		fi
	fi
	# Lint C benchmark files...
	if [[ -z "${skip_c_benchmarks}" ]]; then
		files=$(echo "${changed_files}" | grep '/benchmark/.*\.c$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make check-c-linters > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'Unable to lint C files. Ensure that linters are installed.' >&2
			else
				make C_LINTER=cppcheck CPPCHECK_SUPPRESSIONS_LIST="${cppcheck_benchmarks_suppressions_list}" FILES="${files}" lint-c-files > /dev/null >&2
				if [[ "$?" -ne 0 ]]; then
					echo '' >&2
					echo 'C lint errors for benchmark files.' >&2
					return 1
				fi
			fi
		fi
	fi
	# Lint C test fixtures files...
	if [[ -z "${skip_c_tests_fixtures}" ]]; then
		files=$(echo "${changed_files}" | grep '/test/fixtures/.*\.c$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make check-c-linters > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'Unable to lint C files. Ensure that linters are installed.' >&2
			else
				make C_LINTER=cppcheck CPPCHECK_SUPPRESSIONS_LIST="${cppcheck_tests_fixtures_suppressions_list}" FILES="${files}" lint-c-files > /dev/null >&2
				if [[ "$?" -ne 0 ]]; then
					echo '' >&2
					echo 'C lint errors for test fixtures files.' >&2
					return 1
				fi
			fi
		fi
	fi
	# Lint shell script files...
	if [[ -z "${skip_shell}" ]]; then
		files=$(echo "${changed_files}" | while read -r file; do head -n1 "$file" | grep -q '^\#\!/usr/bin/env bash' && echo "$file"; done | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make check-shell-linters > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'Unable to lint shell script files. Ensure that linters are installed.' >&2
			else
				make FILES="${files}" lint-shell-files > /dev/null >&2
				if [[ "$?" -ne 0 ]]; then
					echo '' >&2
					echo 'Shell script lint errors.' >&2
					return 1
				fi
			fi
		fi
	fi
	# Lint TypeScript declaration files...
	if [[ -z "${skip_typescript_declarations}" ]]; then
		files=$(echo "${changed_files}" | grep '/docs/types/.*\.d.ts$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make TYPESCRIPT_DECLARATIONS_LINTER=eslint FILES="${files}" lint-typescript-declarations-files > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'TypeScript declaration file lint errors.' >&2
				return 1
			fi
		fi
	fi
	# Lint TypeScript declaration test files...
	if [[ -z "${skip_typescript_tests}" ]]; then
		files=$(echo "${changed_files}" | grep '/docs/types/test.ts$' | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make TYPESCRIPT_DECLARATIONS_LINTER=eslint FILES="${files}" ESLINT_TS_CONF="${eslint_typescript_tests_conf}" lint-typescript-declarations-files > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'TypeScript test file lint errors.' >&2
				return 1
			fi
		fi
	fi
	# Lint license headers...
	if [[ -z "${skip_license_headers}" ]]; then
		files=$(echo "${changed_files}" | tr '\n' ' ')
		if [[ -n "${files}" ]]; then
			make FILES="${files}" lint-license-headers-files > /dev/null >&2
			if [[ "$?" -ne 0 ]]; then
				echo '' >&2
				echo 'License header lint errors.' >&2
				return 1
			fi
		fi
	fi

	# TODO: if datapackage.json, validate via schema

	# Re-add files that may have been fixed by linting:
	# shellcheck disable=SC2086
	git add ${changed_files}

	return 0
}

# Main execution sequence.
main() {
	init
	if [[ "$?" -ne 0 ]]; then
		on_error 1
	fi
	check_filenames
	if [[ "$?" -ne 0 ]]; then
		on_error 1
	fi
	run_lint
	if [[ "$?" -ne 0 ]]; then
		on_error 1
	fi
	cleanup
	exit 0
}

# Run main:
main
